处理搜索交互逻辑#1023
Conversation
|
看arco的设计,是需要点确定(调用confirm)才会进行筛选的,感觉这实际上是个bug 初步猜测是因为修改了对象引用导致的 |
|
好的。我会看看 把这个都改掉就应该可以发布 1.2 正式版了 |
现在我已经改好了,但总感觉是运行在bug上 |
|
先不要合并。我晚点再找个时间看一下 |
改好了现在有点尴尬了 不过先这样吧 你说的那个奇怪问题 |
2c5513c to
a829565
Compare
There was a problem hiding this comment.
Pull request overview
本 PR 旨在解决脚本列表搜索过滤功能的交互问题。在之前的版本中,修改搜索类型(自动/名字/代码)时,过滤功能要么无法正常工作,要么只能工作一次。本次修改通过重构搜索过滤逻辑,引入新的 SearchFilter 类来管理搜索状态和缓存,解决了过滤器重复触发和状态不一致的问题。
主要改动:
- 新增
SearchFilter.ts类来统一管理搜索过滤逻辑和缓存 - 重构
ScriptTable组件的搜索过滤实现,使用表格内置的过滤功能 - 移除
ref+onFilterDropdownVisibleChange的手动聚焦方式,改用autoFocus属性 - 优化搜索交互流程,支持搜索类型切换时立即触发过滤
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pages/options/routes/ScriptList/SearchFilter.ts | 新增搜索过滤器类,使用模块级缓存管理搜索结果,提供可扩展的过滤逻辑 |
| src/pages/options/routes/ScriptList/ScriptTable.tsx | 重构表格搜索过滤实现,移除 searchRequest 和 setSearchRequest props,使用内部 SearchFilter 实例处理过滤逻辑 |
| src/pages/options/routes/ScriptList/hooks.tsx | 更新 useScriptSearch hook,引入 SearchFilter 类处理搜索请求,优化过滤状态管理 |
| src/pages/options/routes/ScriptList/components.tsx | 移除 ScriptSearchField 的 React.memo 优化和自定义比较函数,添加 onChange 回调支持实时搜索,修复搜索类型比较运算符 |
| src/pages/options/routes/ScriptList/index.tsx | 传递完整的 scriptList 给表格组件而非预过滤的列表,让表格组件自行处理过滤 |
| src/pages/options/routes/SubscribeList.tsx | 移除 inputRef 和 onFilterDropdownVisibleChange,改用 autoFocus 属性实现自动聚焦 |
| src/pages/components/ScriptStorage/index.tsx | 移除 inputRef 和 onFilterDropdownVisibleChange,改用 autoFocus 属性实现自动聚焦 |
| src/pages/components/ScriptResource/index.tsx | 移除 inputRef 和 onFilterDropdownVisibleChange,改用 autoFocus 属性实现自动聚焦 |
| src/pages/options/routes/utils.tsx | 修改 ListHomeRender 组件,为数组项添加 key 属性以消除 React 警告 |
| export class SearchFilter { | ||
| constructor() {} | ||
| requestFilterResult(req: SearchFilterRequest): void { | ||
| if (req.keyword === lastKeyword) { | ||
| lastReqType = req.type; | ||
| this.onResponse(req, lastResponse); | ||
| } else { | ||
| requestFilterResult({ value: req.keyword }).then((res) => { | ||
| lastReqType = req.type; | ||
| lastKeyword = req.keyword; | ||
| lastResponse = res; | ||
| searchFilterCache.clear(); | ||
| if (res && Array.isArray(res)) { | ||
| for (const entry of res) { | ||
| searchFilterCache.set(entry.uuid, { | ||
| code: entry.code, | ||
| name: entry.name, | ||
| auto: entry.auto, | ||
| }); | ||
| } | ||
| } | ||
| this.onResponse(req, res); | ||
| }); | ||
| } | ||
| } | ||
| onResponse(_req: SearchFilterRequest, _res: SearchFilterResponse): void { | ||
| // placeholder | ||
| } | ||
| checkByUUID(uuid: string): boolean { | ||
| const result = searchFilterCache.get(uuid); | ||
| if (!result) return false; | ||
| switch (lastReqType) { | ||
| case "auto": | ||
| return result.auto; | ||
| case "script_code": | ||
| return result.code; | ||
| case "name": | ||
| return result.name; | ||
| default: | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
新增的 SearchFilter 类缺少类和方法的文档注释。建议添加 JSDoc 注释来说明:
- 类的用途和设计模式(例如使用了模板方法模式)
- 各个方法的参数、返回值和副作用
onResponse方法的回调时机
例如:
/**
* SearchFilter 类用于处理脚本搜索过滤逻辑
* 使用模块级缓存来避免重复的搜索请求
* 子类可以重写 onResponse 方法来处理搜索结果
*/
export class SearchFilter {
/**
* 请求过滤结果
* @param req 搜索请求,包含关键词和搜索类型
*/
requestFilterResult(req: SearchFilterRequest): void {
// ...
}
/**
* 当搜索结果返回时的回调方法(模板方法)
* 子类应重写此方法来处理搜索结果
*/
onResponse(_req: SearchFilterRequest, _res: SearchFilterResponse): void {
// ...
}
}| class ScriptTableSearchFilter extends SearchFilter { | ||
| filterKeys?: SearchFilterKeyEntry[] = undefined; | ||
| setFilterKeys?: SearchFilterKeysSetter = undefined; | ||
| defaultValue = { type: "auto", keyword: "" }; |
There was a problem hiding this comment.
在类中硬编码 defaultValue 作为实例属性可能不够灵活。如果将来需要不同的默认值,需要修改类定义。建议将其作为构造函数参数:
class ScriptTableSearchFilter extends SearchFilter {
filterKeys?: SearchFilterKeyEntry[] = undefined;
setFilterKeys?: SearchFilterKeysSetter = undefined;
constructor(public defaultValue: SearchFilterKeyEntry = { type: "auto", keyword: "" }) {
super();
}
// ...
}| defaultValue = { type: "auto", keyword: "" }; | |
| defaultValue: SearchFilterKeyEntry; | |
| constructor(defaultValue: SearchFilterKeyEntry = { type: "auto", keyword: "" }) { | |
| super(); | |
| this.defaultValue = defaultValue; | |
| } |
| // 表格传入全部脚本,由表格组件处理过滤和排序 | ||
| scriptList={scriptList} |
There was a problem hiding this comment.
注释说明"表格传入全部脚本,由表格组件处理过滤和排序",但这与卡片视图不一致。卡片视图(第112行)仍然传入 filterScriptList(已过滤的列表)。这种不一致可能会让维护者困惑。
建议:
- 统一两种视图的数据传递方式
- 或者在注释中明确说明为什么两种视图采用不同的策略
例如:
// 表格传入全部脚本,由表格组件内部的 Arco Table 过滤功能处理过滤
// 卡片视图传入已过滤的列表,因为卡片不支持内置过滤功能
scriptList={scriptList}| // 静态变数不随重绘重置 | ||
| let lastReqType: SearchType | undefined = undefined; | ||
| let lastKeyword: string = ""; | ||
| let lastResponse: ScriptCode | undefined = undefined; | ||
| const searchFilterCache: Map<string, any> = new Map(); |
There was a problem hiding this comment.
模块级静态变量缺少文档说明。这些全局状态变量(lastReqType、lastKeyword、lastResponse、searchFilterCache)在多个实例间共享,可能导致意外行为。建议添加注释说明:
- 这些变量的用途和生命周期
- 为什么它们是模块级别而不是实例级别
- 多个 SearchFilter 实例共享这些状态的影响
例如:
// 模块级缓存:所有 SearchFilter 实例共享这些状态
// 用于避免重复的搜索请求,提高性能
// 注意:在多个组件同时使用 SearchFilter 时可能导致状态冲突
let lastReqType: SearchType | undefined = undefined;| requestFilterResult(req: SearchFilterRequest): void { | ||
| if (req.keyword === lastKeyword) { | ||
| lastReqType = req.type; | ||
| this.onResponse(req, lastResponse); | ||
| } else { | ||
| requestFilterResult({ value: req.keyword }).then((res) => { | ||
| lastReqType = req.type; | ||
| lastKeyword = req.keyword; | ||
| lastResponse = res; | ||
| searchFilterCache.clear(); | ||
| if (res && Array.isArray(res)) { | ||
| for (const entry of res) { | ||
| searchFilterCache.set(entry.uuid, { | ||
| code: entry.code, | ||
| name: entry.name, | ||
| auto: entry.auto, | ||
| }); | ||
| } | ||
| } | ||
| this.onResponse(req, res); | ||
| }); | ||
| } |
There was a problem hiding this comment.
存在潜在的竞态条件问题。如果用户快速修改搜索条件,多个异步请求可能同时进行,后发的请求可能先返回,导致 lastKeyword、lastResponse 和 searchFilterCache 被旧数据覆盖。建议:
- 使用一个请求计数器或时间戳来跟踪最新请求
- 忽略过时的响应
例如:
private requestId = 0;
requestFilterResult(req: SearchFilterRequest): void {
const currentRequestId = ++this.requestId;
if (req.keyword === lastKeyword) {
// ...
} else {
requestFilterResult({ value: req.keyword }).then((res) => {
if (currentRequestId !== this.requestId) return; // 忽略过时响应
// ...更新状态
});
}
}| class HooksSearchFilter extends SearchFilter { | ||
| onResponse(req: SearchFilterRequest, res: SearchFilterResponse) { | ||
| setLastFilterQuery({ request: req, response: res }); | ||
| } | ||
| } | ||
| const searchFilter = new HooksSearchFilter(); |
There was a problem hiding this comment.
在 useScriptSearch hook 内部定义类并在每次重新渲染时创建新实例会导致性能和状态问题:
- 每次渲染都会创建新的
HooksSearchFilter类定义和实例 - 在 useEffect 中使用的
searchFilter实例在每次渲染时都是新的,可能导致闭包问题
建议:
- 将类定义移到 hook 外部
- 使用
useMemo或useRef来保持实例的稳定性
例如:
// 在 hook 外部定义
class HooksSearchFilter extends SearchFilter {
constructor(private setLastFilterQuery: React.Dispatch<React.SetStateAction<...>>) {
super();
}
onResponse(req: SearchFilterRequest, res: SearchFilterResponse) {
this.setLastFilterQuery({ request: req, response: res });
}
}
// 在 hook 内部
const searchFilter = useMemo(() => new HooksSearchFilter(setLastFilterQuery), []);| filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => { | ||
| // 重绘时更新React参考 | ||
| searchFilter.filterKeys = filterKeys; | ||
| searchFilter.setFilterKeys = setFilterKeys; |
There was a problem hiding this comment.
在 filterDropdown 函数内部更新 searchFilter 实例的属性(filterKeys 和 setFilterKeys)存在问题。由于 searchFilter 在每次渲染时都是新实例(第474行),而 filterDropdown 是在 useMemo 的依赖项中定义的(第476行的 columns),这可能导致:
- 旧的
searchFilter实例的属性被更新,但该实例已经过时 onFilter方法(第536行)引用的是过时的searchFilter实例
建议使用 useRef 来保持 searchFilter 实例稳定:
const searchFilter = useRef(new ScriptTableSearchFilter()).current;| searchFilter.requestFilterResult(searchRequest); | ||
| } | ||
| }, [originMap, scriptList, selectedFilters, tagMap, searchRequest]); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [searchRequest, scriptList]); // scriptList 有改动时也重新查一下最新的结果吧 | ||
|
|
||
| useEffect(() => { | ||
| // 当 filterFuncs 改变时进行 / Filter结果取得时进行 | ||
| // 按 filterFuncs 过滤一次 | ||
| let filterList = scriptList.filter((script) => filterFuncs.every((fn) => fn(script))); | ||
| if (lastFilterQuery) { | ||
| // 再基于关键词过滤一次 | ||
| filterList = filterList.filter((item) => { | ||
| return searchFilter.checkByUUID(item.uuid); |
There was a problem hiding this comment.
在 useEffect 中使用 searchFilter 实例存在问题,因为 searchFilter 在每次渲染时都是一个新实例(第558行)。这会导致:
- useEffect 的依赖项中应该包含
searchFilter,但注释说"searchFilter 参考固定不变"(第582行)是不正确的 - 每次渲染时创建的新实例与上一次渲染时的实例不同,但 useEffect 无法感知到这个变化
建议使用 useRef 或 useMemo 来保持 searchFilter 实例的稳定性:
const searchFilter = useMemo(() => {
class HooksSearchFilter extends SearchFilter {
onResponse(req: SearchFilterRequest, res: SearchFilterResponse) {
setLastFilterQuery({ request: req, response: res });
}
}
return new HooksSearchFilter();
}, []); // 空依赖数组,实例在组件生命周期内保持不变Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
没有看出要使用 export class SearchFilter { 的必要,我看主要是为了防止同样的keywrod重复查询,而且 lastFilterQuery 和 lastReqType 之类的逻辑 跳来跳去的,希望可读性能更好一点 |
一,兩邊用同一個邏輯取信息做filter, 共通部份多只是顯示方式不同 你要整理也可以。不要整理完又一堆bug一堆效能問題就行 |
概述 Descriptions
@cyfung1031
遇到个很奇怪的问题,看了大半天了,可能得研究arco的实现了: https://github.com/arco-design/arco-design/blob/8960a171b5d26d7ce04dbde4a83db70eb13f95e8/components/Table/thead/column.tsx#L70
删除下面这行代码就不能 直接修改 自动/名字/代码 进行筛选,添加上只能修改一次,改过去再改回来不会重新筛选,但是v1.2.0-beta.1 可以无限次筛选
scriptcat/src/pages/options/routes/ScriptList/ScriptTable.tsx
Line 510 in 04253ff
v1.2.0-beta.1: https://github.com/scriptscat/scriptcat/blob/v1.2.0-beta.1/src/pages/options/routes/ScriptList.tsx#L957
79505ef
Jietu20251125-161724-HD.mp4
变更内容 Changes
截图 Screenshots